# -*- coding: utf-8 -*-
from PyQt5.Qt import (QThread, QMoveEvent, QResizeEvent, QPaintEvent, QCloseEvent)
from PyQt5.QtGui import (QIcon, QKeySequence, QMovie)
from PyQt5.QtCore import (QFileInfo, QSize, QPoint, QRect, QSettings, QSize, Qt, QUrl, QTimer, QObject, pyqtSlot, pyqtSignal)
from PyQt5.QtWidgets import (QLabel, QGridLayout, QAction, QWidget, QStackedLayout, QApplication, QFileDialog, QMainWindow, QMessageBox)
from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineView)
from PyQt5.QtWebChannel import QWebChannel
from views import (_webViewFrame, _videoListFrame, _usersListFrame, _publishedFrame, _newuserDialog, _importvideoDialog, _settingsDialog)
from multiprocessing import (Process, Queue, Value)
from models import SQLiteDb
from threading import Thread
import requests
import shutil
import pickle
import time, os
import worker
import helper

class LoadingMask(QWidget):
    def __init__(self, parent):
        super(LoadingMask, self).__init__(parent)
        parent.installEventFilter(self)
        self.label = QLabel(self)
        self.label.setAlignment(Qt.AlignCenter)
        root = QFileInfo(__file__).absolutePath()
        self.movie = QMovie(root + '/images/loading.gif')
        self.label.setMovie(self.movie)
        self.movie.start()
        self.layout = QGridLayout()
        self.layout.setContentsMargins(0,0,0,0)
        self.layout.addWidget(self.label)
        self.setLayout(self.layout)
        self.show_time = 0
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setStyleSheet('background-color:rgba(0,0,0,0.01);')
        self.hide()

    def eventFilter(self, widget, event):
        events = {QMoveEvent, QResizeEvent, QPaintEvent}
        if widget == self.parent():
            if type(event) == QCloseEvent:
                pass
                #self.close()
                #return True
            elif type(event) in events:
                self.moveWithParent()
                return True
        return super(LoadingMask, self).eventFilter(widget, event)

    def moveWithParent(self):
        if self.parent().isVisible():
            self.move(self.parent().geometry().x(), self.parent().geometry().y())
            self.setFixedSize(QSize(self.parent().geometry().width(), self.parent().geometry().height()))
    
    def show(self):
        super(LoadingMask, self).show()
        self.show_time = time.time()
        self.moveWithParent()

    def close(self):
        # 显示时间不够最小显示时间 设置Timer延时删除
        if (time.time() - self.show_time) * 1000 < 3:
            QTimer().singleShot((time.time() - self.show_time)*1000+3, self.close)
        else:
            super(LoadingMask, self).hide()
            super(LoadingMask, self).deleteLater()

class ThreadLogin(QThread):
    # 返回登录账户信息
    _signal = pyqtSignal(str, str, str, str)

    def __init__(self, phone=''):
        super().__init__()
        self._phone = phone

    def run(self):
        state, session = worker.login(self._phone)
        if state == 1:
            userId, userName, userAvatar = worker.getaccount(session)
            if userId is not False:
                self._signal.emit(self._phone, str(userId), userName, userAvatar)
        else:
            #todo: 重新登录处理
            pass

class ThreadCopyfile(QThread):
    # 已复制文件数量 路径 大小 是否失败
    _signal = pyqtSignal(int, str, str, str, str)

    def __init__(self, userids, srcpaths, rootpath):
        super().__init__()
        self._userids = userids
        self._srcpath = srcpaths
        self._rootpath = rootpath

    def run(self):
        failed = 0
        for i, srcpath in enumerate(self._srcpath):
            fileSize = self.format_file_size(os.path.getsize(srcpath))
            for j, uid in enumerate(self._userids):
                error = 'success'
                dstpath = os.path.join(self._rootpath, 'cache', str(uid))
                dstpath_new = os.path.join(dstpath, os.path.basename(srcpath))
                try:
                    if not os.path.exists(dstpath):
                        os.makedirs(dstpath)
                    shutil.copyfile(srcpath, dstpath_new)
                except Exception as e:
                    error = str(e)
                    failed += 1
                self._signal.emit(int((i+1)*(j+1)), str(uid), dstpath_new, fileSize, error)

    def format_file_size(self, fileSize) :
        for count in ['Bytes','KB','MB','GB']:
            if fileSize > -1024.0 and fileSize < 1024.0:
                return "%3.1f %s" % (fileSize, count)
            fileSize /= 1024.0
        return "%3.1f %s" % (fileSize, 'TB')

class ThreadPublish(QThread):
    # 展示正在处理任务消息
    _signal = pyqtSignal(str, str)

    def __init__(self, parent, msgQueue):
        super().__init__()
        self.parent = parent
        self.msgQueue = msgQueue

    def run(self):
        logfile =  '%s/logs/%s.log' % (self.parent.absolutePath, time.strftime('%Y%m%d'))
        file = open(logfile, 'r', encoding='utf-8')
        file.seek(0, 2)
        while True:
            where = file.tell()
            line = file.readline()
            if not line:
                time.sleep(1)
                file.seek(where)
            else:
                # 打印日志
                self._signal.emit('loger', line.strip())

            try:
                # 队列消息
                msg = self.msgQueue.get(False)
                self._signal.emit(msg[0], msg[1])
                if msg[0] == 'finished':
                    break
                if msg[0] == 'relogin':
                    break
            except Exception as e:
                pass


class ProcessPublish(Process):
    # 发布和取回已发布共用进程
    def __init__(self, dbPath, publishState, msgQueue, action, pubCity=None, pubTmee=10):
        super().__init__()
        self.dbPath = dbPath
        self.publishState = publishState
        self.msgQueue = msgQueue
        self.action   = action
        self.pub_city = pubCity
        self.pub_tmee = pubTmee

    def run(self):
        if 'publish' == self.action:
            self.publish()

        if 'fetchpub' == self.action: 
            try:
                self.fetchpub()
            except Exception as e:
                print(str(e))

    def fetchpub(self):
        # 取回用户已发布视频 悄悄的干活
        self.Db = SQLiteDb(self.dbPath)
        userId  = self.publishState
        phone = self.Db.fetchOneUsers(userId)[1]
        items = worker.workslist(phone)
        if items:
            for it in items:
                videoItem = {
                    'userId': userId,
                    'workId': it['workId'],
                    'title': it['title'],
                    'cover': '',
                    'uploadTime': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(it['uploadTime'] / 1000)),
                    'playCount': it['playCount'],
                    'likeCount': it['likeCount'],
                    'commentCount': it['commentCount']
                }
                self.Db.addVideospub(videoItem)
                time.sleep(0.1)

    def publish(self):
        # 发布任务 通过队列消息返回指令
        self.Db = SQLiteDb(self.dbPath)
        self.allnum  = self.Db.countVideos()
        self.allpage = int(int(self.allnum + 100 - 1) / 100)
        if 0 == self.allnum:
            self.msgQueue.put(('finished', ''))
            return True
        
        # 载入栅格、已发布
        self.loadPoi()
        # 缓存用户字典
        _users = {}
        _idx = 0
        for i in range(0, self.allpage):
            if self.publishState.value == 0:
                # 暂存已发布
                self.savePoi()
                break
            dbvideos = self.Db.fetchVideos(limit = 100, page = int(i+1))
            for d in dbvideos:
                if self.publishState.value == 0:
                    # 暂存已发布
                    self.savePoi()
                    break
                vId, userId, userName, filePath, title= int(d[0]), str(d[1]), d[2], d[5], d[7]
                if userId in _users:
                    phone = _users[userId]
                else:
                    phone = self.Db.fetchOneUsers(userId)[1]
                    _users[userId] = phone

                _idx += 1
                self.msgQueue.put(('ok', '正在发布(%s/%s) %s - %s < %s' % (_idx, self.allnum, userName, title, filePath)))

                # 判断是否登录
                login, session = worker.islogin(phone)
                if login == 1:
                    # 调用发布 获取栅格坐标
                    lnglat = self.getPoi()
                    result = worker.publish(session, filePath, title, lnglat[0], lnglat[1])
                    # 删除记录
                    if int(result) == 1:
                        self.Db.delVideos(vid = vId)
                else:
                    self.msgQueue.put(('relogin', phone))
                
                if _idx < self.allnum:
                    for t in range(self.pub_tmee):
                        self.msgQueue.put(('ok', '等待%s秒后继续发布' % (self.pub_tmee - t)))
                        time.sleep(1)

        self.msgQueue.put(('finished', ''))

    def getPoi(self):
        # 队列中取出待发布坐标，判断是否在已发布集合
        lnglat = ['', '']
        if self.pub_city is None:
            return lnglat
        while True:
            if self.pub_queue.empty():
                break
            try:
                got = self.pub_queue.get()
                has, lnglat = got[0], got[1]
                if str(has) not in self.pub_map:
                    self.pub_map[has] = 1
                    break
            except:
                time.sleep(1)

        return lnglat

    def loadPoi(self):
        # 载入待发布栅格队列，已发布集合
        if self.pub_city:
            try:
                if os.path.exists('cache/pubpoi.cks'):
                    with open('cache/pubpoi.cks', 'rb') as f:
                        self.pub_map = pickle.load(f)
                else:
                    self.pub_map = {}
            except:
                self.pub_map = {}
                
            self.pub_queue = Queue()
            hep = helper.Helper()
            res = hep.dumppoi(self.pub_city)
            for key,value in res.items():
                self.pub_queue.put((key,value))

    def savePoi(self):
        # 保存已发布集合
        if self.pub_city:
            with open('cache/pubpoi.cks', 'wb') as f:
                pickle.dump(self.pub_map, f)

class MainWindow(QMainWindow):
    # 版本
    version = 211
    version_str   = 'v2.11'
    version_title = '快手视频发布助手'
    version_about = '\n方便管理多个快手账号，视频内容批量发布' \
        '\n地图栅格多点发布，虚拟定位发布' \
        '\n\n2021.04\nliweimin@taiyuan'

    def __init__(self):
        super(MainWindow, self).__init__()

        # 菜单状态列表
        self.actList = set()
        # 发布状态
        self.publishState = Value('I', 0)
        # 发布任务队列
        self.msgQueue = Queue()
        # 用户已加载
        self.loadedUsers  = False
        # 视频已加载
        self.loadedVideos = False
        self.absolutePath = QFileInfo(__file__).absolutePath()

        self.setWindowTitle('%s %s' % (self.version_title, self.version_str))
        self.setWindowIcon(QIcon(self.absolutePath + '/images/app.png'))
        self.readSettings()
        self.createActions()
        self.createMenus()
        self.createToolBars()
        self.createStatusBar()
        self.createLayouts()
        self.loadDb()

    def loadDb(self):
        if 'cache' not in os.listdir(self.absolutePath):
            os.mkdir(os.path.join(self.absolutePath, 'cache'))

        if 'data' not in os.listdir(self.absolutePath):
            os.mkdir(os.path.join(self.absolutePath, 'data'))

        # data/kuai shou video helper database
        self.dbPath = os.path.join(self.absolutePath, 'data', 'KSVH.db')
        self.Db = SQLiteDb(self.dbPath)

    def videoList(self):
        self.actUnchecked()
        self.storageAct.setChecked(True)
        self.mainLayout.setCurrentIndex(0)
        if self.loadedVideos is False:
            self.loadedVideos = True
            self.videoListFrame.pager()
            dbusers = self.Db.fetchUsers()
            userslist = [(u[0], u[3]) for u in dbusers]
            self.videoListFrame.clearComboItem()
            self.videoListFrame.addComboItem(userslist)

    def usersList(self):
        self.actUnchecked()
        self.listAct.setChecked(True)
        self.mainLayout.setCurrentIndex(1)
        if self.loadedUsers is False:
            self.loadedUsers = True
            dbusers = self.Db.fetchUsers()
            self.usersListFrame.userList.clears()
            self.usersListFrame.addUserList(dbusers)

    def usersAdd(self, phone='', relogin=False):
        if len(str(phone)) > 5:
            # 开一个线程等待扫码登录
            self.tdLogin = ThreadLogin(phone)
            if not relogin:
                self.tdLogin._signal.connect(self._usersAdd)
            self.tdLogin.start()
        else:
            self.adduserDialog = _newuserDialog(self)
            self.adduserDialog.setFixedSize(300, 150)
            self.adduserDialog.setWindowIcon(QIcon(self.absolutePath + '/images/useradd.png'))
            self.adduserDialog.show()

    def _usersAdd(self, phone, userId, userName, userAvatar):
        # 登录成功回调保存
        if phone is not '' and userId is not '':
            # 保存到用户表
            self.Db.addUsers(phone, userId, userName, userAvatar)
            # 添加到表现层
            self.loadedUsers = False
            self.adduserDialog.close()
            self.usersList()

    def videoImport(self):
        dbusers = self.Db.fetchUsers()
        if len(dbusers) == 0:
            QMessageBox().information(None, '提示', '您还没有添加快手号，请先添加快手号', QMessageBox.Yes)
            return False

        self.importvideoDialog = _importvideoDialog(self)
        self.importvideoDialog.setFixedSize(360, 200)
        self.importvideoDialog.setWindowIcon(QIcon(self.absolutePath + '/images/folder.png'))
        self.importvideoDialog.show()
        
        userslist = [(u[0], u[3]) for u in dbusers]
        self.importvideoDialog.addComboItem(userslist)

    def importVideos(self, userids, filepaths):
        self.import_uids = userids
        self.import_fpaths = filepaths
        self.import_count  = len(self.import_fpaths)*len(self.import_uids)

        self.usersidMap = {}
        dbusers  = self.Db.fetchUsers()
        for u in dbusers:
            self.usersidMap[str(u[0])] = str(u[3])

        self.importvideoDialog.close()
        self.showLoading()
        # 开一个线程 文件导入待发布缓存区
        self.tdCopyfile = ThreadCopyfile(self.import_uids, self.import_fpaths,  self.absolutePath)
        self.tdCopyfile._signal.connect(self._importVideos)
        self.tdCopyfile.start()

    def _importVideos(self, idx, uid, fpath, fsize, msg):
        if msg == 'success':
            self.statusBar().showMessage('正在导入 %s' % fpath)

            title = os.path.basename(fpath)[:-4]
            userName = self.usersidMap[uid]
            videoItem = {
                'userId': uid,
                'userName': userName,
                'cityId': '',
                'hashCode': '',
                'filePath': fpath,
                'fileSize': fsize,
                'title': title,
                'cover': ''
            }
            self.Db.addVideos(videoItem)
        else:
            self.statusBar().showMessage('导入 %s 发生错误: %s' % (fpath, msg))
        #导入完成
        if self.import_count == idx:
            # 重新加载视频列表
            self.loadedVideos = False
            self.videoList()
            self.statusBar().showMessage('成功导入[%s]个视频到待发布区' % str(idx), 3000)
            self.LoadingMask.close()

    def publishAction(self):
        #todo: 使用限制 上传成功保存次数，次数超过到期
        if self.license(self._license_, 'check') is False:
            QMessageBox.warning(self, '未授权', '试用版已到期，请使用授权版\n授权版提供更多增强功能，终身免费升级')
            self.buyLicense()
            return True

        self.actUnchecked()
        self.mainLayout.setCurrentIndex(2)

        if self.publishState.value == 0:
            self.publishState.value = 1
            self.publishAct.setText('正在发布')
            self.publishedFrame.showtop()

            # 开线程展示界面
            self.tdPublish = ThreadPublish(self, self.msgQueue)
            self.tdPublish._signal.connect(self._publishAction)
            self.tdPublish.start()
            # 开进程处理任务
            pubCity = self.pub_city if self.pub_grid == 1 else None
            self.psPublish = ProcessPublish(self.dbPath, self.publishState, self.msgQueue, 'publish', pubCity, self.pub_tmee)
            self.psPublish.start()

    def _publishAction(self, msg, args):
        if msg == 'relogin':
            self.publishStop()
            message = '账号[%s]登录凭证失效，请重新登录' % args
            self.statusBar().showMessage(message)
            reply = QMessageBox().information(None, '重新登录', message, QMessageBox.Yes)
            self.usersAdd(phone=args, relogin=True)

        if msg == 'finished':
            self.publishStop()
            self.publishedFrame.toast.setText('发布任务已完成')
            self.statusBar().showMessage('发布任务已完成')

        if msg == 'ok':
            self.publishedFrame.toast.setText(args)
            self.statusBar().showMessage(args)

        # 给日志框转发
        if msg == 'loger':
            self.publishedFrame.printmsg(args)

    def publishStop(self):
        self.publishState.value = 0
        self.publishAct.setText('立即发布')
        self.publishedFrame.hidetop()
        self.loadedVideos = False
        return True

    def fetchVideospub(self, userId):
        # 开子进程取回保存
        self.psPublish = ProcessPublish(self.dbPath, userId, None, 'fetchpub')
        self.psPublish.start()
        self.statusBar().showMessage('已发布视频同步中..', 3000)

    def pubsetTings(self):
        # 发布设置
        self.settingsDialog = _settingsDialog(self)
        self.settingsDialog.setFixedSize(300, 170)
        self.settingsDialog.setWindowIcon(QIcon(self.absolutePath + '/images/setting.png'))
        self.settingsDialog.show()

    def actUnchecked(self):
        for act in self.actList:
            act.setChecked(False)

    def createActions(self):
        self.openAct = QAction(QIcon(self.absolutePath + '/images/folder.png'), "&导入视频", self, 
            triggered=self.videoImport)

        self.storageAct = QAction(QIcon(self.absolutePath + '/images/video.png'), "&待发布视频", self, 
            triggered=self.videoList, 
            checkable=True)

        self.publishAct = QAction(QIcon(self.absolutePath + '/images/play.png'), "&立即发布", self, 
            triggered=self.publishAction)

        self.userAct = QAction(QIcon(self.absolutePath + '/images/useradd.png'), "&添加快手号", self, 
            triggered=self.usersAdd)

        self.listAct = QAction(QIcon(self.absolutePath + '/images/userlist.png'), "&快手号管理", self, 
            triggered=self.usersList, 
            checkable=True)

        self.actList.add(self.storageAct)
        self.actList.add(self.listAct)

        self.openSetting = QAction(QIcon(self.absolutePath + '/images/setting.png'), "&设置", self, 
            triggered=self.pubsetTings)

        self.aboutAct = QAction(QIcon(self.absolutePath + '/images/app.png'), "试用版", self, triggered=self.about)
        self.buyAct   = QAction("购买授权", self, triggered=self.buyLicense)
        self.exitAct  = QAction(QIcon(self.absolutePath + '/images/quit.png'), "退出", self, triggered=self.close)

        if self.license(self._license_, 'check') is True:
            self.aboutAct.setText('授权版')


    def createMenus(self):
        self.userMenu = self.menuBar().addMenu("&账号  ")
        self.userMenu.addAction(self.userAct)
        self.userMenu.addAction(self.listAct)
        self.userMenu.addSeparator();
        self.userMenu.addAction(self.exitAct)

        self.fileMenu = self.menuBar().addMenu("&内容  ")
        self.fileMenu.addAction(self.openAct)
        self.fileMenu.addAction(self.storageAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.publishAct)

        self.helpMenu = self.menuBar().addMenu("&设置  ")
        self.helpMenu.addAction(self.openSetting)

        self.aboutMenu = self.menuBar().addMenu("&关于  ")
        self.aboutMenu.addAction(self.aboutAct)
        self.aboutMenu.addAction(self.buyAct)

    def createToolBars(self):
        self.userToolBar = self.addToolBar("User")
        self.userToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.userToolBar.addAction(self.userAct)
        self.userToolBar.addAction(self.listAct)

        self.videoToolBar = self.addToolBar("Video")
        self.videoToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.videoToolBar.addAction(self.openAct)
        self.videoToolBar.addAction(self.storageAct)
        self.videoToolBar.addAction(self.publishAct)

    def createStatusBar(self):
        self.statusBar().showMessage("Ready")

    def createLayouts(self):
        
        self.videoListFrame = _videoListFrame(self)
        self.usersListFrame = _usersListFrame(self)
        self.publishedFrame = _publishedFrame(self)
        self.webViewFrame   = _webViewFrame(self)

        self.mainLayout = QStackedLayout()
        self.mainLayout.addWidget(self.videoListFrame)
        self.mainLayout.addWidget(self.usersListFrame)
        self.mainLayout.addWidget(self.publishedFrame)
        self.mainLayout.addWidget(self.webViewFrame)

        self.mainWidget = QWidget()
        self.mainWidget.setLayout(self.mainLayout)
        self.setCentralWidget(self.mainWidget)

    def readSettings(self):
        settings = QSettings('Jiuceng', 'ksvtool')
        desktop  = QApplication.desktop()
        posx = int((desktop.width() - 1000) / 2)
        posy = int((desktop.height() - 600) / 2)
        pos  = settings.value('pos', QPoint(posx, posy))
        size = settings.value('size', QSize(1000, 600))
        self.resize(size)
        self.move(pos)
        self._license_ = settings.value('license', '')
        self.pub_tmee = settings.value('pub_tmee', 10)
        self.pub_grid = settings.value('pub_grid', 0)
        self.pub_city = settings.value('pub_city', '')
        self.pub_dist = settings.value('pub_dist', 1)

    def writeSettings(self):
        settings = QSettings('Jiuceng', 'ksvtool')
        settings.setValue('pos', self.pos())
        settings.setValue('size', self.size())
        settings.setValue('pub_tmee', self.pub_tmee)
        settings.setValue('pub_grid', self.pub_grid)
        settings.setValue('pub_city', self.pub_city)
        settings.setValue('pub_dist', self.pub_dist)

    def maybeSave(self):
        if self.publishState.value is 1:
            msgbox = QMessageBox()
            msgbox.setWindowTitle('确认')
            msgbox.setIcon(QMessageBox.Warning)
            msgbox.setWindowIcon(QIcon(self.absolutePath + '/images/app.png'))
            msgbox.setText('发布任务正在运行，是否立即退出？')
            msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            msgbox.button(QMessageBox.Yes).setText('退出')
            msgbox.button(QMessageBox.No).setText('取消')
            ret = msgbox.exec()

            if ret == QMessageBox.Yes:
                return self.publishStop()

            if ret == QMessageBox.No:
                return False

        return True

    def closeEvent(self, event):
        if self.maybeSave():
            self.writeSettings()
            event.accept()
        else:
            event.ignore()

    def about(self):
        self.actUnchecked()
        self.openAct.setChecked(True)
        QMessageBox.about(self, "关于",
            "%s %s %s" % (self.version_title, self.version_str, self.version_about))

    def license(self, val='', act=''):
        if act is 'check':
            status = 0
            try:
                res = requests.get('http://jiucengkeji.com/license/license/%s' % val).json()
                status = res['status']
            except:
                pass

            if status == 0: return False
            return True
        else:
            settings = QSettings('Jiuceng', 'ksvtool')
            settings.setValue('license', val)
            self._license_ = val
            self.aboutAct.setText('授权版')

    def buyLicense(self):
        if self.license(self._license_, 'check') is True:
            QMessageBox.information(self, '已授权', '授权码：%s \n有效期至2099.1.1' % self._license_)
            return True
        self.showLoading()
        self.mainLayout.setCurrentIndex(3)
        self.webViewFrame.load('http://jiucengkeji.com/wxpay')
        self.webViewFrame.webview.loadFinished.connect(lambda: self.LoadingMask.close())

    def showLoading(self):
        self.LoadingMask = LoadingMask(self)
        self.LoadingMask.show()
        #QTimer().singleShot(3500, lambda: self.LoadingMask.close())
        
class handleJS(QObject):
    def __init__(self, app):
        super().__init__()
        self.app = app

    @pyqtSlot(str)
    def license(self, val):
        self.app.license(val)

if __name__ == '__main__':

    import sys
    import multiprocessing
    multiprocessing.freeze_support()

    app = QApplication(sys.argv)
    mainWin = MainWindow()
    # qt5.5之后js调用
    hdjs = handleJS(mainWin)
    chnl = QWebChannel()
    chnl.registerObject('pyjs', hdjs)
    mainWin.webViewFrame.webview.page().setWebChannel(chnl)

    mainWin.show()
    sys.exit(app.exec_())
